package lumis.content.workflow;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import lumis.content.ContentManagerFactory;
import lumis.content.core.Content;
import lumis.content.core.ContentLocale;
import lumis.content.core.ContentPublication;
import lumis.content.core.ContentVersion;
import lumis.content.table.ContentTableSource;
import lumis.content.workflow.WorkflowTransitionConfig.WorkflowLockAction;
import lumis.doui.source.ISourceContext;
import lumis.doui.source.Source;
import lumis.doui.source.SourceContext;
import lumis.doui.source.SourceFactory;
import lumis.portal.AccessDeniedException;
import lumis.portal.PortalException;
import lumis.portal.PortalObjectNotFoundException;
import lumis.portal.UnexpectedException;
import lumis.portal.acl.PortalPermissions;
import lumis.portal.authentication.SessionConfig;
import lumis.portal.cache.PortalCache;
import lumis.portal.group.IGroupManager;
import lumis.portal.manager.ManagerException;
import lumis.portal.manager.ManagerFactory;
import lumis.portal.principal.PrincipalConfig;
import lumis.portal.serviceinstance.ServiceInstanceConfig;
import lumis.portal.serviceinstance.acl.ServiceInstancePermissions;
import lumis.portal.transaction.AbstractTransactionObserver;
import lumis.portal.user.UserConfig;
import lumis.portal.util.PortalUtilInternal;
import lumis.util.ITransaction;
import lumis.util.PortalUtil;
import lumis.util.TextUtil;
import lumis.util.XmlUtil;
import lumis.util.security.acl.AccessControlEntry;
import lumis.util.security.acl.AccessControlList;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

/**
 * The default implementation for IWorkflowManager.
 * @see IWorkflowManager
 * @since 4.0.0
 * @version $Revision: 13311 $ $Date: 2011-08-05 16:42:38 -0300 (Fri, 05 Aug 2011) $
 */
public class WorkflowManager implements IWorkflowManager
{
	/** A cache that maps serviceInstanceId to workflowId. */
	private PortalCache<String> serviceInstanceIdWorkflowIdCache = new PortalCache<String>("lumis.content.workflow.ServiceInstanceId-WorkflowId");

	private IWorkflowDao workflowDao = new WorkflowDaoHib();
	
	/**
	 * Returns the {@link WorkflowConfig} instance to save in the register.
	 * @param sessionConfig the user session information.
	 * @param workflowConfigPath workflow configuration path.
	 * @param transaction the transaction for persistence access.
	 * @return the {@link WorkflowConfig} instance to save in the register.
	 * @since 4.0.5
	 */
	protected WorkflowConfig getWorkflowToRegister(SessionConfig sessionConfig, String workflowConfigPath, ITransaction transaction) throws ManagerException, PortalException
	{
		try
		{
			// read the workflow definition document
			Document workflowDocument = PortalUtilInternal.getDefinitionDocument(workflowConfigPath + "/workflowdefinition.xml");
			
			// TODO: validate the workflow configuration

			Node workflowXmlNode = XmlUtil.selectSingleNode("workflowDefinition/workflow", workflowDocument);
			if (workflowXmlNode != null)
			{
				// add the baseUrl Node
				XmlUtil.addTextNode(workflowXmlNode, "baseUrl", workflowConfigPath);

				// add the default resource structure, if necessary
				if (!XmlUtil.nodeExists("resource", workflowXmlNode))
					XmlUtil.addNode(workflowXmlNode, "resource");
				if (!XmlUtil.nodeExists("resource/className", workflowXmlNode))
					XmlUtil.addTextNode(workflowXmlNode, "resource", "className", "lumis.content.workflow.WorkflowStringResource");
				if (!XmlUtil.nodeExists("resource/stringsUrlPrefixes", workflowXmlNode))
					XmlUtil.addNode(workflowXmlNode, "resource", "stringsUrlPrefixes");
				if (!XmlUtil.nodeExists("resource/stringsUrlPrefixes/stringsUrlPrefix", workflowXmlNode))
					XmlUtil.addTextNode(workflowXmlNode, "resource/stringsUrlPrefixes", "stringsUrlPrefix", "strings/strings");
								
				WorkflowConfig workflowConfig = new WorkflowConfig();
				workflowConfig.setDefinition(workflowXmlNode);
				return workflowConfig;
			}
			else
				return null;
		}
		catch (PortalException e)
		{
			if (e.getCause() instanceof SAXException)
			{
				throw new ManagerException("STR_ERROR_REGISTERING_WORKFLOW;" + TextUtil.escapeLocalizationParameter(workflowConfigPath) + ";" + 
						TextUtil.escapeLocalizationParameter(e.getLocalizedMessage(sessionConfig.getLocale())), e);
			}
			else
			{
				throw e;
			}
		}
		catch (Exception err)
		{
			throw new UnexpectedException(err);
		}		
	}
	
	/**
	 * Register a workflow.
	 */
	public void register(SessionConfig sessionConfig, String workflowConfigPath, ITransaction transaction) throws ManagerException, PortalException
	{
		if(!ManagerFactory.getPortalAclManager().checkPermission(sessionConfig, PortalPermissions.MANAGE_PORTAL, transaction))
			throw new AccessDeniedException();
		
		WorkflowConfig workflowConfig = getWorkflowToRegister(sessionConfig, workflowConfigPath, transaction);
				
		if (workflowConfig != null)
		{
			// check if there is already a workflow with that id
			try
			{
				getWorkflow(sessionConfig, workflowConfig.getId(), transaction);
				throw new WorkflowException("STR_ALREADY_EXISTS_A_WORKFLOW_WITH_THE_SPECIFIED_WORKFLOWS_ID");
			}
			catch (PortalObjectNotFoundException e)
			{
				// workflow may not exist
			}

			workflowDao.add(workflowConfig, transaction);
		}
	}
	
	/**
	 * Updates the register of a workflow.
	 */
	public void updateRegister(SessionConfig sessionConfig, String workflowId, ITransaction transaction) throws ManagerException, PortalException
	{
		if(!ManagerFactory.getPortalAclManager().checkPermission(sessionConfig, PortalPermissions.MANAGE_PORTAL, transaction))
			throw new AccessDeniedException();

		WorkflowConfig workflowConfig = getWorkflow(sessionConfig, workflowId, transaction);
		WorkflowConfig updatedWorkflow = getWorkflowToRegister(sessionConfig, workflowConfig.getBaseUrl(), transaction);
		
		if( (workflowConfig != null && updatedWorkflow != null) 
				&& (workflowConfig.getBaseUrl().equals(updatedWorkflow.getBaseUrl()) && !workflowConfig.getId().equals(updatedWorkflow.getId())))
			throw new WorkflowException("STR_CANNOT_CHANGE_WORKFLOW_ID;"+workflowConfig.getName());
		
		if (updatedWorkflow != null)
			workflowDao.update(updatedWorkflow, transaction);
	}

	public void unregister(SessionConfig sessionConfig, String workflowId, ITransaction transaction) throws PortalException
	{
		if(!ManagerFactory.getPortalAclManager().checkPermission(sessionConfig, PortalPermissions.MANAGE_PORTAL, transaction))
			throw new AccessDeniedException();
		
		workflowDao.delete(workflowId, transaction);
	}

	public Collection<WorkflowConfig> getAllWorkflows(SessionConfig sessionConfig, ITransaction transaction) throws PortalException
	{
		Collection<WorkflowConfig> workflows = new ArrayList<WorkflowConfig>();
		Collection<String> workflowIds = workflowDao.getIds(transaction);
		for (String workflowId: workflowIds)
		{
			workflows.add(getWorkflow(sessionConfig, workflowId, transaction));
		}
		return workflows;
	}
	
	public WorkflowConfig getWorkflow(SessionConfig sessionConfig, String workflowId, ITransaction transaction) throws PortalException
	{
		if (workflowId == null)
			return null;
		
		return workflowDao.get(workflowId, transaction);
	}
	
	public List<WorkflowTransitionConfig> getTransitionsByStateId(SessionConfig sessionConfig, String workflowId, String stateId, ITransaction transaction) throws PortalException
	{
		WorkflowStateConfig stateConfig = getState(sessionConfig, workflowId, stateId, transaction);
		return stateConfig.getTransitions();
	}
	
	/**
	 * Checks if any of the roles have permission to the specified workflow resource.
	 * @param sessionConfig the current session information.
	 * @param roles a collection with the roles ids.
	 * @param permissionable a permissionable workflow resource.
	 * @param scopes the scopes to have the permission verified
	 * @param transaction the transaction for persistence access.
	 * @return true if any roles has permission to the specified workflow resource, false otherwise.
	 * @throws PortalException
	 */
	protected boolean hasStatePermission(SessionConfig sessionConfig, Collection<String> roles, AbstractPermissionable permissionable, EnumSet<StateScope> scopes, ITransaction transaction) throws PortalException
	{
		if (UserConfig.USER_SYSTEM_ID.equals(sessionConfig.getUserId()))
			return true;
		
		Iterator<StateScope> sIterator = scopes.iterator();
		
		while (sIterator.hasNext())
		{
			StateScope scope = sIterator.next();
			Collection<String> permissions = permissionable.getPermissions(scope);
			for (String permission : permissions)
			{
				if (roles.contains(permission))
					return true;
			}
		}
		return false;
	}
	
	/**
	 * Checks if any of the roles have permission to the specified workflow resource.
	 * @param sessionConfig the current session information.
	 * @param roles a collection with the roles ids.
	 * @param permissionable a permissionable workflow resource.
	 * @param scopes the scopes to have the permission verified
	 * @param transaction the transaction for persistence access.
	 * @return true if any roles has permission to the specified workflow resource, false otherwise.
	 * @throws PortalException
	 */
	@SuppressWarnings("unchecked")
	protected boolean hasTransitionPermission(SessionConfig sessionConfig, Collection<String> roles, AbstractPermissionable permissionable, EnumSet<TransitionScope> transitionScopes, ITransaction transaction) throws PortalException
	{
		if (UserConfig.USER_SYSTEM_ID.equals(sessionConfig.getUserId()))
			return true;
		
		Iterator<TransitionScope> sIterator = transitionScopes.iterator();
		
		while (sIterator.hasNext())
		{
			TransitionScope scope = sIterator.next();
			Collection<String> permissions = permissionable.getPermissions(scope);
			for (String permission : permissions)
			{
				if (roles.contains(permission))
					return true;
			}
		}
		return false;
	}
	
	protected Collection<String> getPrincipalRoles(SessionConfig sessionConfig, String serviceInstanceId, String principalId, ITransaction transaction) throws PortalException
	{
		if (UserConfig.USER_SYSTEM_ID.equals(principalId))
		{
			WorkflowConfig workflowConfig = getServiceInstanceWorkflow(sessionConfig, serviceInstanceId, transaction);
			Collection<String> roles = new ArrayList<String>(workflowConfig.getRoles().size());
			for (WorkflowRoleConfig role: workflowConfig.getRoles())
				roles.add(role.getId());
			return roles;
		}
		else
		{
			Collection<String> groups = ManagerFactory.getGroupManager().getMemberGroups(sessionConfig, principalId, true, transaction);
			Collection<String> principals = new ArrayList<String>(groups.size() + 1);
			principals.add(principalId);
			principals.addAll(groups);
			
			Collection<String> roles = workflowDao.getRolesByPrincipals(serviceInstanceId, principals, transaction);
			return roles;
		}
	}
	
	private boolean isContentAssignedToPrincipalOrHisGroup(SessionConfig sessionConfig, ContentWorkflowMetaData contentWorkflowMetaData, String principalId, ITransaction transaction) throws PortalException
	{
		String assignedTo = contentWorkflowMetaData.getAssignedTo();
		if (assignedTo == null || principalId.equals(assignedTo))
			return true;
		else
		{
			Collection<String> groups = ManagerFactory.getGroupManager().getMemberGroups(sessionConfig, principalId, true, transaction);
			for (String groupId: groups)
			{
				if (groupId.equals(assignedTo))
					return true;
			}				
		}
		return false;
	}
	
	/**
	* Checks if any of the principal have permission to the specified workflow resource.
	* @param sessionConfig the current session information.
	* @param principalId the principal id.
	* @param permissionable a permissionable workflow resource.
	* @param contentLocale the content locale specific object. If null, 
	* assignedTo scoped permissions will be ignored.
	* @param serviceInstanceId the content service instance id.
	* @param transaction the transaction for persistence access.
	* @return true if the principal has permission to access the specified workflow resource, false otherwise.
	*/
	protected boolean hasStatePermission(SessionConfig sessionConfig, String principalId, 
			AbstractPermissionable permissionable, ContentLocale contentLocale, 
			String serviceInstanceId, ITransaction transaction) throws PortalException
	{
		ContentWorkflowMetaData contentWorkflowMetaData = getContentWorkflowMetaData(sessionConfig, contentLocale, transaction);
		// check scope all permissions
		Collection<String> roles = getPrincipalRoles(sessionConfig, serviceInstanceId, principalId, transaction);
		if (hasStatePermission(sessionConfig, roles, permissionable, EnumSet.of(StateScope.all), transaction))
			return true;

		if (contentLocale != null)
		{			
			// use the permissions with scope assignedTo only if the content is assigned to null, userId, or one of his groups
			if (isContentAssignedToPrincipalOrHisGroup(sessionConfig, contentWorkflowMetaData, principalId, transaction))
			{	
				if (hasStatePermission(sessionConfig, roles, permissionable, EnumSet.of(StateScope.assignedTo), transaction))
					return true;
			}
		}
		return false;
	}
	
	/**
	* Checks if any of the principal have permission to the specified workflow resource.
	* @param sessionConfig the current session information.
	* @param principalId the principal id.
	* @param permissionable a permissionable workflow resource.
	* @param contentLocale the content locale specific object. If null, 
	* assignedTo scoped permissions will be ignored.
	* @param serviceInstanceId the content service instance id.
	* @param transaction the transaction for persistence access.
	* @return true if the principal has permission to access the specified workflow resource, false otherwise.
	*/
	protected boolean hasTransitionPermission(SessionConfig sessionConfig, String principalId, 
			AbstractPermissionable permissionable, ContentLocale contentLocale, 
			String serviceInstanceId, ITransaction transaction) throws PortalException
	{
		// check scope all permissions
		Collection<String> roles = getPrincipalRoles(sessionConfig, serviceInstanceId, principalId, transaction);
		if (hasTransitionPermission(sessionConfig, roles, permissionable, EnumSet.of(TransitionScope.all), transaction))
			return true;
		
		if (contentLocale != null)
		{			
			// use the permissions with scope assignedTo only if the content is assigned to null, userId, or one of his groups
			ContentWorkflowMetaData contentWorkflowMetaData = getContentWorkflowMetaData(sessionConfig, contentLocale, transaction);
			if (isContentAssignedToPrincipalOrHisGroup(sessionConfig, contentWorkflowMetaData, principalId, transaction))
			{
				if (hasTransitionPermission(sessionConfig, roles, permissionable, EnumSet.of(TransitionScope.assignedTo), transaction))
					return true;
			}
		}
		return false;
	}

	@Deprecated
	public Collection<WorkflowTransitionConfig> getAccessibleTransitions(SessionConfig sessionConfig, String itemId, String serviceInstanceId, String sourceId, ITransaction transaction) throws PortalException
	{
		if (itemId != null)
		{
			ContentLocale contentLocale = ContentManagerFactory.getContentManager().getContentVersionByItemId(itemId).getContentLocale();
			return getAccessibleTransitions(sessionConfig, contentLocale, transaction);
		}
		else
		{
			WorkflowConfig workflowConfig = getSourceWorkflow(sessionConfig, serviceInstanceId, sourceId, transaction);
			if (workflowConfig != null)
			{
				WorkflowStateConfig stateConfig = workflowConfig.getStartState();
				return getAccessibleTransitions(sessionConfig, stateConfig, serviceInstanceId, EnumSet.of(TransitionScope.assignedTo), transaction); 
			}
			else
			{
				return Collections.<WorkflowTransitionConfig>emptyList();
			}
		}
	}

	public Collection<WorkflowTransitionConfig> getAccessibleTransitions(
			SessionConfig sessionConfig, ContentLocale contentLocale,
			ITransaction transaction) throws PortalException
	{
		if (sessionConfig == null)
			throw new IllegalArgumentException("sessionConfig must not be null");
		if (contentLocale == null)
			throw new IllegalArgumentException("contentLocale must not be null");
		if (transaction == null)
			throw new IllegalArgumentException("transaction must not be null");
		
		Content content = contentLocale.getContent();
		String serviceInstanceId = content.getServiceInstanceId();
		String sourceId = content.getSourceId();
		
		WorkflowConfig workflowConfig = getSourceWorkflow(sessionConfig, serviceInstanceId, sourceId, transaction);
		if (workflowConfig != null)
		{
			ContentWorkflowMetaData contentWorkflowMetaData = getContentWorkflowMetaData(sessionConfig, contentLocale, transaction);
			if (contentWorkflowMetaData != null)
			{
				boolean useAssignedToPermissions = isContentAssignedToPrincipalOrHisGroup(
						sessionConfig, contentWorkflowMetaData, sessionConfig.getUserId(), transaction);
				WorkflowStateConfig state = workflowConfig.getState(contentWorkflowMetaData.getStateId());
				if(useAssignedToPermissions)
				{
					return getAccessibleTransitions(sessionConfig, state, serviceInstanceId, EnumSet.of(TransitionScope.assignedTo), transaction);
				}
				else
				{
					return getAccessibleTransitions(sessionConfig, state, serviceInstanceId, EnumSet.of(TransitionScope.all), transaction);
				}
			}
		}
		return Collections.emptyList();
	}

	public Collection<WorkflowTransitionConfig> getAccessibleTransitions(SessionConfig sessionConfig, WorkflowStateConfig state,
			String serviceInstanceId, boolean includeScopeAssignedTo, 
			ITransaction transaction) throws PortalException
	{
		// arguments assertions
		if (sessionConfig == null)
			throw new IllegalArgumentException("sessionConfig must not be null");
		if (state == null)
			throw new IllegalArgumentException("state must not be null");
		if (serviceInstanceId == null)
			throw new IllegalArgumentException("serviceInstanceId must not be null");
		if (transaction == null)
			throw new IllegalArgumentException("transaction must not be null");
		
		Collection<WorkflowTransitionConfig> result = new ArrayList<WorkflowTransitionConfig>();
		Collection<String> roles = getPrincipalRoles(sessionConfig, serviceInstanceId, sessionConfig.getUserId(), transaction);
		
		List<WorkflowTransitionConfig<TransitionScope>> transitions = state.getTransitions();
		for (WorkflowTransitionConfig transitionConfig: transitions)
		{
			if (includeScopeAssignedTo)
			{
				result = getAccessibleTransitions(sessionConfig, state, serviceInstanceId, EnumSet.of(TransitionScope.all, TransitionScope.assignedTo), transaction);
			}
			else
			{
				result = getAccessibleTransitions(sessionConfig, state, serviceInstanceId, EnumSet.of(TransitionScope.all), transaction);
			}
			
		}
		return result;
	}

	@SuppressWarnings("unchecked")
	public Collection<WorkflowStateConfig> getAccessibleStates(SessionConfig sessionConfig, String serviceInstanceId, boolean includeScopeAll, boolean includeScopeAssignedTo, ITransaction transaction) throws PortalException
	{
		WorkflowConfig workflowConfig = getServiceInstanceWorkflow(sessionConfig, serviceInstanceId, transaction);
		if (workflowConfig == null)
			return Collections.<WorkflowStateConfig>emptyList();
		
		List<WorkflowStateConfig> workflowStates = workflowConfig.getStates();
		Collection<WorkflowStateConfig> accessibleStates = new ArrayList<WorkflowStateConfig>(workflowStates.size()); 
		
			if (includeScopeAll && includeScopeAssignedTo)
			{
				accessibleStates = getAccessibleStates(sessionConfig, serviceInstanceId, EnumSet.of(StateScope.all, StateScope.assignedTo), transaction);
			}
			else if (!includeScopeAll && includeScopeAssignedTo)
			{
				accessibleStates = getAccessibleStates(sessionConfig, serviceInstanceId, EnumSet.of(StateScope.assignedTo), transaction);
			}
			else if (includeScopeAll && !includeScopeAssignedTo)
			{
				accessibleStates = getAccessibleStates(sessionConfig, serviceInstanceId, EnumSet.of(StateScope.all), transaction);
			}

		return accessibleStates;
	
	}

	public WorkflowStateConfig getState(SessionConfig sessionConfig, String workflowId, String stateId, ITransaction transaction) throws PortalException
	{
		WorkflowConfig workflowConfig = getWorkflow(sessionConfig, workflowId, transaction);
		return workflowConfig.getState(stateId);
	}

	public WorkflowTransitionConfig getTransition(SessionConfig sessionConfig, String workflowId, String transitionId, ITransaction transaction) throws PortalException
	{
		String[] ids = transitionId.split("\\.", 2);
		WorkflowStateConfig stateConfig = getState(sessionConfig, workflowId, ids[0], transaction);
		return stateConfig.getTransition(transitionId);
	}
	
	private WorkflowConfig getSourceWorkflow(SessionConfig sessionConfig, String serviceInstanceId, String sourceId, ITransaction transaction) throws PortalException
	{
		ServiceInstanceConfig serviceInstanceConfig = ManagerFactory.getServiceInstanceManager().get(serviceInstanceId, transaction);
		ISourceContext sourceContext = new SourceContext(sessionConfig, serviceInstanceConfig, transaction);
		ContentTableSource source = (ContentTableSource)SourceFactory.createSource(sourceId, sourceContext);
		return source.getWorkflowConfig();
	}
	
	public WorkflowConfig getServiceInstanceWorkflow(SessionConfig sessionConfig, String serviceInstanceId, ITransaction transaction) throws PortalException
	{
		String workflowId = serviceInstanceIdWorkflowIdCache.get(serviceInstanceId);
		if (workflowId == null)
		{
			workflowId = workflowDao.getIdByServiceInstance(serviceInstanceId, transaction);
			serviceInstanceIdWorkflowIdCache.put(serviceInstanceId, workflowId);
		}
		return getWorkflow(sessionConfig, workflowId, transaction);
	}
	
	public void setServiceInstanceWorkflow(SessionConfig sessionConfig, String serviceInstanceId, String workflowId, ITransaction transaction) throws PortalException
	{
		final String serviceId = ManagerFactory.getServiceInstanceManager().get(serviceInstanceId, transaction).getServiceId();
		final ServiceInstancePermissions serviceInstancePermissions = ManagerFactory.getServiceInstanceAclManager().getPermissions(
				sessionConfig, serviceId, transaction);
		if (!ManagerFactory.getServiceInstanceAclManager().checkPermission(sessionConfig, serviceInstanceId,
				serviceInstancePermissions.MANAGE_SERVICE_INSTANCE, transaction))
			throw new AccessDeniedException();

		WorkflowConfig workflow = getServiceInstanceWorkflow(sessionConfig, serviceInstanceId, transaction);
		if (workflow == null && workflowId == null)
			return; // no change
		if (workflow != null && workflow.getId().equals(workflowId))
			return; // no change
		
		
		if(workflow == null)
		{
			ServiceInstanceConfig serviceInstanceConfig = ManagerFactory.getServiceInstanceManager().get(serviceInstanceId, transaction);
			SourceContext sourceContext = new SourceContext(sessionConfig, serviceInstanceConfig, transaction);
			Collection<String> sourceIds = workflowDao.getSourceIdsWithContent(serviceInstanceId, transaction);
			for(String sourceId : sourceIds)
			{
				Source s = SourceFactory.createSource(sourceId, sourceContext);
				if(s instanceof ContentTableSource)
				{
					ContentTableSource s2 = (ContentTableSource)s;
					if(s2.isWorkflowSupported())
						throw new WorkflowException("STR_CANNOT_CHANGE_SERVICE_INSTANCE_WORKFLOW_WITH_CONTENT");
				}
			}
		}
		
		// if the service instance has any data, the workflow may not be changed
		if (workflowDao.serviceInstanceHasAnyContentWorkflowMetaData(serviceInstanceId, transaction))
			throw new WorkflowException("STR_CANNOT_CHANGE_SERVICE_INSTANCE_WORKFLOW_WITH_CONTENT");
		
		// change the service instance's workflow
		workflowDao.setServiceInstanceWorkflow(serviceInstanceId, workflowId, transaction);

		// if a workflow was assigned, set default permissions
		if (workflowId != null)
		{
			// get workflow roles
			WorkflowConfig workflowConfig = ContentManagerFactory.getWorkflowManager().getServiceInstanceWorkflow(sessionConfig, serviceInstanceId, transaction);
			List<WorkflowRoleConfig> workflowRoles = workflowConfig.getRoles();
			
			// set all principals with service instance manage security permission to have all workflow roles permissions
			Collection<WorkflowPermission> workflowPermissions = new ArrayList<WorkflowPermission>(workflowRoles.size());
			AccessControlList acl = ManagerFactory.getServiceInstanceAclManager().get(sessionConfig, serviceInstanceId, transaction);
			ServiceInstanceConfig serviceInstanceConfig = ManagerFactory.getServiceInstanceManager().get(serviceInstanceId, transaction);
			ServiceInstancePermissions permissions = ManagerFactory.getServiceInstanceAclManager().getPermissions(sessionConfig, serviceInstanceConfig.getServiceId(), transaction);
			for (AccessControlEntry entry: acl.getEntries())
			{
				if (entry.checkPermission(permissions.MANAGE_SERVICE_INSTANCE))
				{
					for (WorkflowRoleConfig role: workflowRoles)
					{
						WorkflowPermission permission = new WorkflowPermission();
						permission.setPrincipalId(entry.getPrincipalId());
						permission.setServiceInstanceId(serviceInstanceId);
						permission.setWorkflowId(workflowConfig.getId());
						permission.setType(WorkflowPermission.PERMISSION_TYPE_ALLOW);
						permission.setRole(role.getId());
						workflowPermissions.add(permission);
					}
				}
			}
			ContentManagerFactory.getWorkflowManager().setWorkflowPermissions(sessionConfig, serviceInstanceId, workflowPermissions, transaction);			
		}
		
		// remove serviceInstance-workflow reference from cache
		serviceInstanceIdWorkflowIdCache.remove(serviceInstanceId, transaction);
	}
	
	public void destroyServiceInstanceWorkflow(SessionConfig sessionConfig, String serviceInstanceId, ITransaction transaction) throws PortalException
	{
		// TODO: authenticate userSessionId and make sure user has rights to
		// delete the service instance

		WorkflowConfig workflow = getServiceInstanceWorkflow(sessionConfig, serviceInstanceId, transaction);
		if (workflow != null)
		{
			// delete the service instance workflow meta data
			deleteAllContentWorkflowMetaData(sessionConfig, serviceInstanceId, transaction);
			
			// remove service instance to workflow relationship
			workflowDao.setServiceInstanceWorkflow(serviceInstanceId, null, transaction);

			// remove data from cache
			serviceInstanceIdWorkflowIdCache.remove(serviceInstanceId, transaction);
		}
	}
	
	public void deleteAllContentWorkflowMetaData(SessionConfig sessionConfig, String serviceInstanceId, ITransaction transaction) throws PortalException
	{
		// TODO: authenticate userSessionId and make sure user has rights to
		// delete the service instance content
		
		// delete the service instance workflow meta data 
		workflowDao.deleteAllContentWorkflowMetaData(serviceInstanceId, transaction);
	}
	
	public Collection<WorkflowPermission> getWorkflowPermissions(SessionConfig sessionConfig, String serviceInstanceId, ITransaction transaction) throws PortalException
	{
		return workflowDao.getWorkflowPermissions(serviceInstanceId, transaction);
	}
	
	public void setWorkflowPermissions(SessionConfig sessionConfig, String serviceInstanceId, Collection<WorkflowPermission> permissions, ITransaction transaction) throws PortalException
	{
		final String serviceId = ManagerFactory.getServiceInstanceManager().get(serviceInstanceId, transaction).getServiceId();
		final ServiceInstancePermissions serviceInstancePermissions = ManagerFactory.getServiceInstanceAclManager().getPermissions(
				sessionConfig, serviceId, transaction);
		if (!ManagerFactory.getServiceInstanceAclManager().checkPermission(sessionConfig, serviceInstanceId,
				serviceInstancePermissions.MANAGE_SERVICE_INSTANCE_SECURITY, transaction))
			throw new AccessDeniedException();

		workflowDao.setWorkflowPermissions(serviceInstanceId, permissions, transaction);
	}
	
	public ContentWorkflowMetaData getContentWorkflowMetaData(SessionConfig sessionConfig, 
			ContentLocale contentLocale, ITransaction transaction) throws PortalException
	{
		return workflowDao.getContentWorkflowMetaData(contentLocale.getId(), transaction);
	}

	@Deprecated
	public ContentWorkflowMetaData getContentWorkflowMetaData(SessionConfig sessionConfig, String itemId, String serviceInstanceId, String sourceId, ITransaction transaction) throws PortalException
	{
		try
		{
			ContentVersion version = ContentManagerFactory.getContentManager().getContentVersionByItemId(itemId);
			return getContentWorkflowMetaData(sessionConfig, version.getContentLocale(), transaction);
		}
		catch (PortalObjectNotFoundException e)
		{
			return null;
		}
	}
	
	@Deprecated
	public void createNewContentWorkflowMetaData(SessionConfig sessionConfig,
			String itemId, String serviceInstanceId, String sourceId,
			String assignedTo, ITransaction transaction) throws PortalException
	{
		ContentVersion version = ContentManagerFactory.getContentManager().getContentVersionByItemId(itemId);
		createNewContentWorkflowMetaData(sessionConfig, version.getContentLocale(), assignedTo, transaction);
	}
	
	public void createNewContentWorkflowMetaData(SessionConfig sessionConfig, 
			ContentLocale contentLocale, String assignedTo, ITransaction transaction) throws PortalException
	{
		Content content = contentLocale.getContent();
		String serviceInstanceId = content.getServiceInstanceId();
		String sourceId = content.getSourceId();
		WorkflowConfig workflow = getSourceWorkflow(sessionConfig, serviceInstanceId, sourceId, transaction);
		if (workflow != null)
		{
			// check if the user has access to the workflow's start state
			WorkflowStateConfig startState = workflow.getStartState();
			String currentUserId = sessionConfig.getUserId();
			Collection<String> userRoles = getPrincipalRoles(sessionConfig, serviceInstanceId, currentUserId, transaction);
			if (!hasStatePermission(sessionConfig, userRoles, startState, EnumSet.of(StateScope.all, StateScope.assignedTo), transaction))
				throw new AccessDeniedException("STR_ACCESS_DENIED");
			
			// create a new content workflow metadata for the content added
			ContentWorkflowMetaData contentWorkflowMetaData = new ContentWorkflowMetaData();
			contentWorkflowMetaData.setWorkflowMetaDataId(PortalUtil.generateNewGuid());
			contentWorkflowMetaData.setStateId(startState.getId());
			contentWorkflowMetaData.setAssignedTo(assignedTo);
			contentWorkflowMetaData.setContentLocaleId(contentLocale.getId());			
			
			// persist the contentWorkflowMetaData
			workflowDao.add(contentWorkflowMetaData, transaction);
		}
	}
	
	@Deprecated
	public void deleteContentWorkflowMetaData(SessionConfig sessionConfig, String itemId, String serviceInstanceId, String sourceId, ITransaction transaction) throws PortalException
	{
		try
		{
			ContentVersion version = ContentManagerFactory.getContentManager().getContentVersionByItemId(itemId);
			deleteContentWorkflowMetaData(sessionConfig, version.getContentLocale(), transaction);
		}
		catch (PortalObjectNotFoundException e)
		{
			// ignored
		}
	}
	
	public void deleteContentWorkflowMetaData(SessionConfig sessionConfig,
			ContentLocale contentLocale, ITransaction transaction)
			throws PortalException
	{
		workflowDao.deleteContentWorkflowMetaData(contentLocale.getId(), transaction);
	}
	
	@Deprecated
	public void executeAction(SessionConfig sessionConfig, String itemId, String serviceInstanceId, String sourceId, String performedTransitionId, String newAssignedTo, ITransaction transaction) throws PortalException
	{
		ContentVersion version = ContentManagerFactory.getContentManager().getContentVersionByItemId(itemId);
		executeAction(sessionConfig, version.getContentLocale(), performedTransitionId, newAssignedTo, transaction);
	}

	public void executeAction(SessionConfig sessionConfig,
			ContentLocale contentLocale, String performedTransitionId,
			String newAssignedTo, ITransaction transaction)
			throws PortalException
	{
		Content content = contentLocale.getContent();
		String sourceId = content.getSourceId();
		String serviceInstanceId = content.getServiceInstanceId();
		WorkflowConfig workflow = getSourceWorkflow(sessionConfig, serviceInstanceId, sourceId, transaction);
		if (workflow == null) 
			return; // if there is no workflow, then do nothing
		
		String currentUserId = sessionConfig.getUserId();

		ContentWorkflowMetaData contentWorkflowMetaData = getContentWorkflowMetaData(sessionConfig, contentLocale, transaction);
		//if new content
		if (contentWorkflowMetaData == null)
		{
			// first create the contentWorkflowMetaData for the content, assigned to the current user
			createNewContentWorkflowMetaData(sessionConfig, contentLocale, currentUserId, transaction);
			
			// get the created contentWorkflowMetaData
			contentWorkflowMetaData = getContentWorkflowMetaData(sessionConfig, contentLocale, transaction);
			assert contentWorkflowMetaData != null;			

			// the "keep same value" assigned to for a new content is null
			if (ASSIGNED_TO_KEEP_SAME_VALUE.equals(newAssignedTo))
				newAssignedTo = null;
		}
		else
		{
			// for a content that already exists, if newAssignedTo was set to keep same value, 
			// set it with the current assignedTo value
			if (ASSIGNED_TO_KEEP_SAME_VALUE.equals(newAssignedTo))
				newAssignedTo = contentWorkflowMetaData.getAssignedTo();			
		}

		if (performedTransitionId == null)
		{
			if ((newAssignedTo == null && contentWorkflowMetaData.getAssignedTo() == null) ||
					(newAssignedTo != null && newAssignedTo.equals(contentWorkflowMetaData.getAssignedTo())))
			{
				return; // nothing to do, no action done
			}
		}
		
		WorkflowStateConfig currentState = workflow.getState(contentWorkflowMetaData.getStateId());
		if (currentState == null)
			throw new UnexpectedException("STR_WORKFLOW_IN_UNKNOWN_STATE");
		
		WorkflowTransitionConfig transition;
		if (performedTransitionId != null)
		{
			transition = currentState.getTransition(performedTransitionId);
			
			// check if the current user has permission to execute the transition 
			if (!hasTransitionPermission(sessionConfig, currentUserId, transition, contentLocale, serviceInstanceId, transaction))
				throw new AccessDeniedException("STR_ACCESS_DENIED");
			
			// check if the newAssignedTo user has permission to the final state
			WorkflowStateConfig finalState  = workflow.getState(transition.getFinalStateId());
			if (newAssignedTo != null)
			{
				if (finalState == null)
					throw new UnexpectedException("STR_WORKFLOW_TRANSITION_FINAL_STATE_NOT_FOUND");
				
				Collection<String> roles = getPrincipalRoles(sessionConfig, serviceInstanceId, newAssignedTo, transaction);
				if (!hasStatePermission(sessionConfig, roles, finalState, EnumSet.of(StateScope.all, StateScope.assignedTo), transaction))
					throw new WorkflowException("STR_ASSIGNED_PRINCIPAL_DOES_NOT_HAVE_PERMISSION_TO_ACCESS_NEW_CONTENT_STATE");
			}
			
			// make the transition corresponding changes in the contentWorkflowMetaData
			contentWorkflowMetaData.setAssignedTo(newAssignedTo);
			contentWorkflowMetaData.setStateId(finalState.getId());
		}
		else
		{
			transition = null;
			
			// check if the newAssignedTo user has permission to the current state
			if (newAssignedTo != null)
			{
				Collection<String> roles = getPrincipalRoles(sessionConfig, serviceInstanceId, newAssignedTo, transaction);
				if (!hasStatePermission(sessionConfig, roles, currentState, EnumSet.of(StateScope.all, StateScope.assignedTo), transaction))
					throw new WorkflowException("STR_ASSIGNED_PRINCIPAL_DOES_NOT_HAVE_PERMISSION_TO_ACCESS_CURRENT_CONTENT_STATE");
			}
			
			// update the assignedTo in the contentWorkflowMetaData
			contentWorkflowMetaData.setAssignedTo(newAssignedTo);
		}
		
		// call workflow action handlers pre process action
		for (IWorkflowActionHandler actionHandler: workflow.getActionHandlers())
		{
			actionHandler.preProcessAction(sessionConfig, contentLocale, transition, contentWorkflowMetaData, transaction);
		}
		
		// persist the changes in the contentWorkflowMetaData
		workflowDao.update(contentWorkflowMetaData, transaction);

		// if state after action is archive, the content will have no published version
		WorkflowStateConfig stateAfterAction = workflow.getState(contentWorkflowMetaData.getStateId());
		if (stateAfterAction.isArchived())
			contentLocale.setPublishedVersion(null);

		//releases lock if: the current lock owner does not have access to the content in the next state 
		//or if the transition is configured to always unlock
		if(transition != null && WorkflowLockAction.unlock.equals(transition.getLockAction()) || 
				!ContentManagerFactory.getWorkflowManager().hasEditPermission(sessionConfig, contentLocale, transaction))
		{
			final String contentLocaleId = contentLocale.getId();
			transaction.addObserver(new AbstractTransactionObserver()
			{
				@Override
				public void afterCommit() throws PortalException
				{
					ContentManagerFactory.getContentManager().releaseLock(contentLocaleId);
				}
			});
			
		}
		
		// update publication state
		for (ContentPublication publication: contentLocale.getActiveVersion().getPublications())
			publication.updateCalculatedFields();
	}
	
	@Deprecated
	public boolean hasEditPermission(SessionConfig sessionConfig, String itemId, String serviceInstanceId, String sourceId, ITransaction transaction) throws PortalException
	{
		ContentVersion contentVersion = ContentManagerFactory.getContentManager().getContentVersionByItemId(itemId);
		return hasEditPermission(sessionConfig, contentVersion.getContentLocale(), transaction);
	}
	
	public boolean hasEditPermission(SessionConfig sessionConfig, ContentLocale contentLocale, ITransaction transaction) throws PortalException
	{
		String serviceInstanceId = contentLocale.getContent().getServiceInstanceId();
		String sourceId = contentLocale.getContent().getSourceId();
		WorkflowConfig workflowConfig = getSourceWorkflow(sessionConfig, serviceInstanceId, sourceId, transaction);
		if (workflowConfig == null)
			return true;
		
		ContentWorkflowMetaData contentWorkflowMetaData = getContentWorkflowMetaData(sessionConfig, contentLocale, transaction);
		if (contentWorkflowMetaData == null)
			return true;

		String currentUserId = sessionConfig.getUserId();
		WorkflowStateConfig currentState = workflowConfig.getState(contentWorkflowMetaData.getStateId());
		return hasStatePermission(sessionConfig, currentUserId, currentState, contentLocale, serviceInstanceId, transaction);
	}
	
	private Collection<String> getPrincipalIdsByPermission(SessionConfig sessionConfig, String serviceInstanceId, Collection<String> roles, int permissionType, ITransaction transaction) throws PortalException
	{
		// get principals with permission to access the state
		Set<String> principalIds = new HashSet<String>();
		principalIds.addAll(workflowDao.getPrincipalIdByServiceInstanceIdAndRoles(serviceInstanceId, roles, permissionType, transaction));
		
		// for each principal that is a group, get all members recursively
		Set<String> membersSet = new HashSet<String>();
		for (String principalId: principalIds)
		{
			PrincipalConfig principal = ManagerFactory.getPrincipalManager().get(sessionConfig, principalId, transaction);
			if (principal != null && principal.getType() == PrincipalConfig.TYPE_GROUP)
			{
				Collection<String> members = ManagerFactory.getGroupManager().getMembers(sessionConfig, principalId, IGroupManager.FILTER_TYPE_USERS_AND_GROUPS, true, transaction);
				membersSet.addAll(members);
			}
		}
		principalIds.addAll(membersSet);
		
		return principalIds;
	}

	@Deprecated
	public Collection<String> getPossibleAssignedToForAction(SessionConfig sessionConfig, String itemId, String serviceInstanceId, String sourceId, String transitionId, ITransaction transaction) throws PortalException
	{
		WorkflowConfig workflowConfig = getServiceInstanceWorkflow(sessionConfig, serviceInstanceId, transaction);
		if (workflowConfig == null)
			return Collections.<String>emptyList();

		// get contentWorkflowMetaData
		ContentWorkflowMetaData contentWorkflowMetaData;
		try
		{
			ContentVersion contentVersion = ContentManagerFactory.getContentManager().getContentVersionByItemId(itemId);
			contentWorkflowMetaData = getContentWorkflowMetaData(sessionConfig, contentVersion.getContentLocale(), transaction);
		}
		catch (PortalObjectNotFoundException e)
		{
			contentWorkflowMetaData = null;
		}
		
		// get current state
		WorkflowStateConfig currentState;
		if (contentWorkflowMetaData != null)
			currentState = workflowConfig.getState(contentWorkflowMetaData.getStateId());
		else
			currentState = workflowConfig.getStartState();

		// discover the final state
		WorkflowStateConfig finalState;
		if (transitionId != null)
		{
			// find final state id for transition
			WorkflowTransitionConfig transitionConfig = currentState.getTransition(transitionId);
			String finalStateId = transitionConfig.getFinalStateId();
			finalState = workflowConfig.getState(finalStateId);
		}
		else
			finalState = currentState;
		
		return getPossibleAssignedTo(sessionConfig, serviceInstanceId, finalState, transaction);
	}
	
	public Collection<String> getPossibleAssignedTo(SessionConfig sessionConfig, 
			String serviceInstanceId, WorkflowStateConfig workflowState, 
			ITransaction transaction) throws PortalException
	{
		Set<String> roles = new HashSet<String>();
		roles.addAll(workflowState.getPermissions(StateScope.all));
		roles.addAll(workflowState.getPermissions(StateScope.assignedTo));
		Collection<String> allowedPrincipalIds = getPrincipalIdsByPermission(sessionConfig, serviceInstanceId, roles, WorkflowPermission.PERMISSION_TYPE_ALLOW, transaction);
		Collection<String> deniedPrincipalIds = getPrincipalIdsByPermission(sessionConfig, serviceInstanceId, roles, WorkflowPermission.PERMISSION_TYPE_DENY, transaction);
		allowedPrincipalIds.removeAll(deniedPrincipalIds);
		
		return allowedPrincipalIds;
	}
	
	@Deprecated
	public boolean contentMayBePublished(SessionConfig sessionConfig, String itemId, String serviceInstanceId, String sourceId, ITransaction transaction) throws PortalException
	{
		ContentVersion version = ContentManagerFactory.getContentManager().getContentVersionByItemId(itemId);
		return contentMayBePublished(sessionConfig, version.getContentLocale(), transaction);
	}
	
	public boolean contentMayBePublished(SessionConfig sessionConfig, ContentLocale contentLocale, ITransaction transaction) throws PortalException
	{
		Content content = contentLocale.getContent();
		String serviceInstanceId = content.getServiceInstanceId();
		String sourceId = content.getSourceId();
		WorkflowConfig workflowConfig = getSourceWorkflow(sessionConfig, serviceInstanceId, sourceId, transaction);
		if (workflowConfig != null)
		{
			ContentWorkflowMetaData contentWorkflowMetaData = getContentWorkflowMetaData(sessionConfig, contentLocale, transaction);
			WorkflowStateConfig currentState;
			if (contentWorkflowMetaData != null)
				currentState = workflowConfig.getState(contentWorkflowMetaData.getStateId());
			else
				currentState = workflowConfig.getStartState();
			
			return currentState.isPublished();
		}
		else
			return true;
	}

	public boolean autoArchivePublishedContent(ContentLocale contentLocale, 
			ITransaction transaction) throws PortalException
	{
		SessionConfig sessionConfig = ManagerFactory.getAuthenticationManager().impersonate(UserConfig.USER_SYSTEM_ID);
		try
		{
			ContentVersion version = contentLocale.getActiveVersion();
			
			// it should be auto archived only if it is currently after the 
			// publishing period for all publications
			for (ContentPublication publication: version.getPublications())
			{
				Date publishEndDateTime = publication.getPublishEndDateTime();
				if (publishEndDateTime == null || !publishEndDateTime.before(new Date()))
					return false;
			}
			
			// get current state
			ContentWorkflowMetaData contentWorkflowMetaData = getContentWorkflowMetaData(sessionConfig, contentLocale, transaction);
			if (contentWorkflowMetaData == null)
				return false;
			Content content = contentLocale.getContent();
			String serviceInstanceId = content.getServiceInstanceId();
			if (serviceInstanceId == null)
				return false;
			WorkflowConfig workflowConfig = getServiceInstanceWorkflow(sessionConfig, serviceInstanceId, transaction);
			WorkflowStateConfig currentState = workflowConfig.getState(contentWorkflowMetaData.getStateId());
			
			// if not in a published state, or already in an archived state, nothing to do
			if (!currentState.isPublished() || currentState.isArchived())
				return false;
			
			// search for a transition to an archival state
			List<WorkflowTransitionConfig> transitions = currentState.getTransitions();
			for (WorkflowTransitionConfig transition: transitions)
			{
				String finalStateId = transition.getFinalStateId();
				WorkflowStateConfig finalState = workflowConfig.getState(finalStateId);
				if (finalState.isArchived())
				{
					// check if the assigned to has permission to the final state,
					// if he doesn't the assigned to will be null.
					String assignedTo = contentWorkflowMetaData.getAssignedTo();
					if (assignedTo != null)
					{
						Collection<String> roles = getPrincipalRoles(sessionConfig, serviceInstanceId, assignedTo, transaction);
						if (!hasStatePermission(sessionConfig, roles, finalState, EnumSet.of(StateScope.all, StateScope.assignedTo), transaction))
							assignedTo = null;
					}
					
					// execute the transition
					executeAction(sessionConfig, version.getItemId(), 
							serviceInstanceId, content.getSourceId(), 
							transition.getId(), assignedTo, transaction);
					
					return true;
				}
			}
			
			return false;
		}
		finally
		{
			ManagerFactory.getAuthenticationManager().endImpersonation(sessionConfig);
		}
	}
	
	/**
	 * Removes all workflow permissions entries where there does not exist
	 * an ACL entry for the same principal in the same service instance.
	 * Called when ACLs are updated.
	 * @since 5.0.0
	 */
	public void removeWorkflowPermissionsWithoutAcl() throws PortalException
	{
		workflowDao.removeWorkflowPermissionsWithoutAcl();
	}

	/**
	 * Returns a map of distinct principals that is assigned to content metadata in the given service instance. 
	 * 
	 * @param sessionConfig the user session information.
	 * @param serviceInstanceId the service instance's id.
	 * @param transaction the transaction for persistence access.
	 * @return map of principals.
	 * 
	 * @since 5.6.1
	 */
	public Map<String, String> getMetadataPrincipals(SessionConfig sessionConfig, String serviceInstanceId, ITransaction transaction) throws PortalException 
	{
		return workflowDao.getContentMetadataPrincipals(serviceInstanceId, transaction);
	}
	
	@SuppressWarnings("unchecked")
	public Collection<WorkflowStateConfig> getAccessibleStates(SessionConfig sessionConfig, String serviceInstanceId, EnumSet<StateScope> scopes, ITransaction transaction) throws PortalException
	{
		WorkflowConfig workflowConfig = getServiceInstanceWorkflow(sessionConfig, serviceInstanceId, transaction);
		if (workflowConfig == null)
			return Collections.<WorkflowStateConfig>emptyList();

		String userId = sessionConfig.getUserId();
		Collection<String> roles = getPrincipalRoles(sessionConfig, serviceInstanceId, userId, transaction);
		
		List<WorkflowStateConfig> workflowStates = workflowConfig.getStates();
		Collection<WorkflowStateConfig> accessibleStates = new ArrayList<WorkflowStateConfig>(workflowStates.size()); 
		for (WorkflowStateConfig state: workflowStates)
		{
			if (hasStatePermission(sessionConfig, roles, state, scopes, transaction))
				accessibleStates.add(state);
		}
		return accessibleStates;
	}

	@SuppressWarnings("unchecked")
	public Collection<WorkflowTransitionConfig> getAccessibleTransitions(SessionConfig sessionConfig, WorkflowStateConfig<?> state, String serviceInstanceId, EnumSet<TransitionScope> scopes,
			ITransaction transaction) throws PortalException
	{
		Collection<WorkflowTransitionConfig> result = new ArrayList<WorkflowTransitionConfig>();
		Collection<String> roles = getPrincipalRoles(sessionConfig, serviceInstanceId, sessionConfig.getUserId(), transaction);

		for (WorkflowTransitionConfig transitionConfig: state.getTransitions())
		{
			if (hasTransitionPermission(sessionConfig, roles, transitionConfig, scopes, transaction))
				result.add(transitionConfig);
		}
		return result;
	}
}
